// ==UserScript==
// @name           ShackNavigationKeys
// @namespace      http://dodob.shackspace.com/greasemonkey/
// @description    Keyboard shortcuts for comments navigation and other things
// @include        http://*.shacknews.com/laryn.x?*
// @include        http://shacknews.com/laryn.x?*
// ==/UserScript==

/*
	2008-11-27
		* Added event.metaKey ignore for Macs.
	2008-07-11
		* No jQuery fun.
	2008-06-27
		* Fix input->text in check_event_target
		* Override contentWindow's check_event_target (for a/z)
		* renamed to Shack - Hotkeys
	2008-06-08
		* re-assigned some shortcuts. Hit '?' for help.
	2008-03-27
		* check_event_target Security Manager Veto (Fx 2.0.0.13)
	2008-03-24
		* Cosmetic changes.
	2008-03-23
		* Prettier Help, Overview.
		* Active Root now visible in Overview :)
	2008-03-22
		* Help added.
		* Overview added.
	2008-03-17
		* 'X' collapse instead of 'x'.
		* '<', '>' prev next page keys.
		* 1,2,3... keys XPath contains selector when looking for postbody.
	2008-03-14
		* Refresh (f) now works.
	2008-03-13
		* Initial release.
	@TODO
		* Nicer root scroll (not always scrolling root to top.)
		* Faster j/k; faster everything in general.
		
		Overview stuff:
		* Send mousescroll to document.
		* Reply count ?
		* Excerpts should fill width of overview window.
*/
if (contentWindow.script_enabled('navigation_keys') == "true") {
(function() {

 	const DEBUG = false;
	const CONTEXT_RATIO = 0.02;
	const EXCERPT_LENGTH = 100;

	const OVERVIEW_ITEM_COLOR = '';
	const OVERVIEW_ITEM_COLOR_SEL = 'rgb(34, 34, 34)';

	const TRANSPARENT_OVERVIEW = false; 
	const TRANSPARENT_HELP = true;

	//$ = contentWindow.jQuery;

	if (DEBUG) {
		function getTime() { benchmarkTimer = new Date(); return benchmarkTimer.getTime(); }
		var benchmarkTimer = null;
		var scriptStartTime = getTime();
	}
	
	// Simulate a mouse click. From Gmail Macros.
	function simulateClick(node, eventType) {
		var event = node.ownerDocument.createEvent('MouseEvents');
		event.initMouseEvent(eventType,
			true,	// can bubble
			true,	// cancellable
			node.ownerDocument.defaultView,
			1,		// clicks
			50, 50,	// screen coords
			50, 50,	// client coords
			false, false, false, false,	// control, alt, shift, meta
			0,	// button
			node);
		node.dispatchEvent(event);
	}

	// Try calculate Y Pos with getBoxObjectFor.
	function calculateYPosBox(ele) {
		var totaly;
		if (document.getBoxObjFor) {
			totaly = document.getBoxObjFor(ele);
		} else {
			totaly = ele.offsetTop;
			while (ele = ele.offsetParent) {
				totaly += ele.offsetTop;
			}
		}
		return totaly;
	}

	function calculateYPos(ele) {
		var totaly = ele.offsetTop;
		while (ele = ele.offsetParent) {
			totaly += ele.offsetTop;
		}
		return totaly;
	}

	function isVisible(ele) {
		var eleY = calculateYPos(ele);

		if ((eleY < window.pageYOffset) || (eleY > window.pageYOffset + window.innerHeight)) {
			return false;
		} else {
			return true;
		}
	}

	// Get the first visible rootEle that matches expr on the page, or the first offpage if none.
	function getFirstVisible(expr, rootEle) {
		var items = document.evaluate(
			expr,
			rootEle,
			null,
			XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
			null);

		var lastNonVisible;
		for (var i = 0, item = null; item = items.snapshotItem(i); i++) {
			if (isVisible(item)) {
				return item;
			} else if (calculateYPos(item) > window.pageYOffset + window.innerHeight) {
				return item;
			}
			lastNonVisible = item;
		}

		//return null;
		return lastNonVisible;
	}

	function xGetFirst(expr, rootEle) {
		var firstItem = document.evaluate(
			expr,
			rootEle,
			null,
			XPathResult.FIRST_ORDERED_NODE_TYPE,
			null).singleNodeValue;
		return firstItem;	// takes care of null
	}

	function xGetLast(expr, rootEle) {
		var items = document.evaluate(
			expr,
			rootEle,
			null,
			XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
			null);
		return items.snapshotItem(items.snapshotLength-1); // returns null if err
	}

	function xGetMiddle(expr, rootEle) {
		var items = document.evaluate(
			expr,
			rootEle,
			null,
			XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
			null);
		return items.snapshotItem((items.snapshotLength - 1) / 2); // returns null if err
	}

	// Get first visible highlight, avoiding collapsed ones
	function getFirstVisibleHighlight(rootid, anchorid, isDown) {
		var root = document.getElementById('root_' + rootid);
		var anchor = document.getElementById('item_' + anchorid);
		var retval;
		//alert('anchor is ' + item);

		var threshold = 0;
		if (root.className.indexOf('capped') != -1) {
			var capnote = xGetFirst('.//p[@class="capnote"]', root);
			threshold = calculateYPosBox(capnote) + 5; // FIVE IS A MAGIC NUMBER, WTF5
		}
		//alert('threshold: ' + threshold);

		var items = document.evaluate(
			'.//li[starts-with(@id, "item_")]',
			root,
			null,
			XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
			null);
		//alert('num items found: ' + items.snapshotLength);

		var found = false;
		for (var i = 0, item = null; item = items.snapshotItem(i); i++) {
			if (item == anchor) {
				found = true;
				if (!isDown) { break; }
			} else if (found) {
				//alert(threshold + ', ' + calculateYPos(item));
				if (calculateYPosBox(item) > threshold) {
					//alert("cap Y: " + threshold + " ; highlight Y: " + calculateYPosBox(item));
					return item;
				}
			}
			retval = item;
		}

		return retval;
	}

	function getNextRoot(currentRoot) {
		var nextRoot = currentRoot.nextSibling;
		while (nextRoot) {
			if ( (!(nextRoot.className)) || (nextRoot.className.indexOf('root') == -1) ) {
				nextRoot = nextRoot.nextSibling;
			} else {
				break;
			}
		}
		return nextRoot;
	}

	function getPrevRoot(currentRoot) {
		var prevRoot = currentRoot.previousSibling;
		while (prevRoot) {
			if ( (!(prevRoot.className)) || (prevRoot.className.indexOf('root') == -1) ) {
				prevRoot = prevRoot.previousSibling;
			} else {
				break;
			}
		}
		return prevRoot;
	}

	// Get nextRoot or prevRoot of sLastClickedRoot.
	function getNewRoot(isDown) {
		var newRoot = null;
		var firstVisible = getFirstVisible('.//div[contains(@class, "root")]', document.getElementById('recent'));

		// firstVisible will contain the first visible, or the first offpage if there are none.

		if (contentWindow.sLastClickedRoot == -1) {

			if (firstVisible) { newRoot = (isDown) ? firstVisible : getPrevRoot(firstVisible); }

		} else {

			var lastClickedRoot = document.getElementById('root_' + contentWindow.sLastClickedRoot);
			if (isVisible(lastClickedRoot)) {
				newRoot = (isDown) ? getNextRoot(lastClickedRoot) : getPrevRoot(lastClickedRoot);

			} else {
				if (firstVisible) { newRoot = (isDown) ? firstVisible : getPrevRoot(firstVisible); }
			}
		}

		return newRoot;
	}

	// Filter for shackimgloader and shackpostget links
	function xGetLinks(expr, rootEle) {
		var arrItems = new Array();
		var items = document.evaluate(
			expr,
			rootEle,
			null,
			XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
			null);

		for (var i = 0, item = null; item = items.snapshotItem(i); i++) {
			arrItems.push(item);
		}
		return arrItems;
	}

	// Filter for shackimgloader and shackpostget links
	function xGetLinksFiltered(expr, rootEle) {
		var arrItems = new Array();
		var items = document.evaluate(
			expr,
			rootEle,
			null,
			XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
			null);

		for (var i = 0, item = null; item = items.snapshotItem(i); i++) {
			if (item.className.indexOf('shackimgloader') != -1
				|| item.className.indexOf('shackpostget') != -1) {
				arrItems.push(item);
			}
		}
		return arrItems;
	}

	function getItemId(item) {
		var x = item;
		while (item) {
			if (item.tagName == 'LI') {
				var id = item.id;
				id = id.replace(/item_/, '');
				return id;
			}
			item = item.parentNode;
		}
		return null;
	}

	function toggleHelp() {
		var help = document.getElementById('shackhotkeys_help');
		var helpBkg = document.getElementById('shackhotkeys_help_bkg');

		if (help) {
			if (help.className.indexOf('hidden') != -1) {
				help.setAttribute( 'class',
					help.getAttribute('class').replace('hidden', '').replace(/^\s\s*/, '').replace(/\s\s*$/, ''));
				if (helpBkg) {
					helpBkg.setAttribute( 'class',
						helpBkg.getAttribute('class').replace('hidden', '').replace(/^\s\s*/, '').replace(/\s\s*$/, ''));
				}
				if (GM_setValue) { GM_setValue('showHelp', true); }
			} else {
				help.setAttribute('class', help.getAttribute('class') + ' hidden');
				if (helpBkg) { helpBkg.setAttribute('class', helpBkg.getAttribute('class') + ' hidden'); }
				if (GM_setValue) { GM_setValue('showHelp', false); }
			}
			return;
		}
		
		help = document.createElement('div');
		help.innerHTML = "<div class=\"shackhotkeys_help_title\">Help: Keyboard Shortcuts</div>\
<span class=\"shackhotkeys_help_subtitle\">Thread Navigation</span>\
<br /><span class=\"jt_yellow\"><b>j</b></span> / <span class=\"jt_yellow\"><b>k</b></span> - Next/Previous Thread\
<br /><span class=\"jt_yellow\"><b>h</b></span> / <span class=\"jt_yellow\"><b>m</b></span> / <span class=\"jt_yellow\"><b>l</b></span> - First/Middle/Last Thread on page\
<br /><span class=\"jt_yellow\"><b>&lt;</b></span> / <span class=\"jt_yellow\"><b>&gt;</b></span> - Previous/Next Page\
<br />\
<span class=\"shackhotkeys_help_subtitle\">Acting on a thread</span>\
<br /><span class=\"jt_yellow\"><b>X</b></span> - Collapse entire thread (toggle)\
<br /><span class=\"jt_yellow\"><b>f</b></span> - Refresh entire thread\
<br />\
<span class=\"shackhotkeys_help_subtitle\">Sub-thread navigation</span>\
<br /><span class=\"jt_yellow\"><b>a</b></span> / <span class=\"jt_yellow\"><b>z</b></span> - Open Next/Previous Post in thread\
<br /><span class=\"jt_yellow\"><b>J</b></span> / <span class=\"jt_yellow\"><b>K</b></span> - Next/Previous Post in thread (highlight)\
<br /><span class=\"jt_yellow\"><b>v</b></span> - Open/close highlighted post (toggle)\
<br /><span class=\"jt_yellow\"><b>q</b></span> - Select first post in thread\
<br />\
<span class=\"shackhotkeys_help_subtitle\">Acting on opened post</span>\
<br /><span class=\"jt_yellow\"><b>r</b></span> - Reply to post\
<br /><span class=\"jt_yellow\"><b>s</b></span> - Show all spoilers in post\
<!-- <br /><span class=\"jt_yellow\"><b>l</b></span> - LOL this post (Shacklol) -->\
<!-- <br /><span class=\"jt_yellow\"><b>m</b></span> - Message this user (Message This User) -->\
<br /><span class=\"jt_yellow\"><b>0</b></span> - Load/Hide all images (Image Loader)\
<br /><span class=\"jt_yellow\"><b>1</b></span>-<span class=\"jt_yellow\"><b>9</b></span> - Load/Hide Postget or Image Loader link\
<br />\
<span class=\"shackhotkeys_help_subtitle\">Other</span>\
<br /><span class=\"jt_yellow\"><b>C</b></span> - New Comment\
<!-- <br /><span class=\"jt_yellow\"><b>e</b></span>, <span class=\"jt_yellow\"><b>i</b></span> - Toggle Comments Overview Menu -->\
<br /><span class=\"jt_yellow\"><b>?</b></span> - Toggle Help Menu";

		helpBkg = help.cloneNode(true);
		helpBkg.id = 'shackhotkeys_help_bkg';
		help.id = 'shackhotkeys_help';
		if (TRANSPARENT_HELP) {
			helpBkg.style.backgroundColor = 'rgb(20, 20, 20)';
			document.body.appendChild(helpBkg);
			help.style.backgroundColor = 'transparent';
		} else {
			help.style.backgroundColor = 'rgb(0, 0, 0)';
		}
		document.body.appendChild(help);
		if (GM_setValue) { GM_setValue('showHelp', true); }
	}

	function toggleOverview() {

		var overview = document.getElementById('shackhotkeys_overview');
		var overviewBkg = document.getElementById('shackhotkeys_overview_bkg');

		if (overview) {
			if (overview.className.indexOf('hidden') != -1) {
				overview.setAttribute( 'class',
					overview.getAttribute('class').replace('hidden', '').replace(/^\s\s*/, '').replace(/\s\s*$/, ''));
				if (overviewBkg) {
					overviewBkg.setAttribute( 'class',
						overviewBkg.getAttribute('class').replace('hidden', '').replace(/^\s\s*/, '').replace(/\s\s*$/, ''));
				}
				if (GM_setValue) { GM_setValue('showOverview', true); }
			} else {
				overview.setAttribute('class', overview.getAttribute('class') + ' hidden');
				if (overviewBkg) { overviewBkg.setAttribute('class', overviewBkg.getAttribute('class') + ' hidden'); }
				if (GM_setValue) { GM_setValue('showOverview', false); }
			}
			return;
		}

		const OVERVIEW_COLOR = 'rgb(75, 75, 75)';
		const OVERVIEW_CLOSE_COLOR = '';
		const OVERVIEW_CLOSE_COLOR_HOVER = 'rgb(60, 60, 60)';
		const OVERVIEW_ITEM_COLOR_HOVER = 'rgb(60, 60, 60)';

		if (DEBUG) {
			var ov_benchmarkTimer = null;
			var ov_scriptStartTime = getTime();
		}

		var thisAuthor = document.evaluate(
			'.//a[contains(@class, "username")]/text()',
			document.getElementById('masthead'),
			null,
			XPathResult.FIRST_ORDERED_NODE_TYPE,
			null).singleNodeValue.data.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
		//alert(thisAuthor);

		var roots = document.evaluate(
				//'.//div[@class="threads"]/div[@class="root"]/ul/li[starts-with(@id, "item_")]',
				//'./div/div/div[@class="threads"]/div[@class="root"]/ul/li[starts-with(@id, "item_")]',
				'./div/div/div[@class="threads"]/div[starts-with(@id, "root_")]/ul/li[starts-with(@id, "item_")]',
				document.getElementById('recent'),
				null,
				XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
				null);
		//alert(roots.snapshotLength);

		overview = document.createElement('div');
		overview.innerHTML =
				'<div class="shackhotkeys_overview_title">Comments Overview</div>'
				+ '<div class="shackhotkeys_overview_close">close</div>';
		overview.lastChild.addEventListener('click', toggleOverview, false);
		overview.lastChild.addEventListener('mouseover', function(event) {
			this.style.backgroundColor = OVERVIEW_CLOSE_COLOR_HOVER;
		}, false);
		overview.lastChild.addEventListener('mouseout', function(event) {
			this.style.backgroundColor = OVERVIEW_CLOSE_COLOR;
		}, false);

		for (var i = 0, root = null; root = roots.snapshotItem(i); i++) {
			var overviewItem = document.createElement('div');
			var postExcerpt = '';
			var author;
			var numReplies;
			var fpmod;

			var postText = document.evaluate(
					'.//div[contains(@class, "postbody")]//text()',
					root,
					null,
					XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
					null);
					//XPathResult.STRING_TYPE,
					//null).stringValue;

			for (var j = 0; j < postText.snapshotLength; j++) {
				postExcerpt += postText.snapshotItem(j).data;
				if (postExcerpt.length > EXCERPT_LENGTH) {
					postExcerpt = postExcerpt.substring(0, EXCERPT_LENGTH);
					postExcerpt += '...';
					break;
				}
			}

			author = document.evaluate(
				'./div[contains(@class, "fullpost")]/div[contains(@class, "postmeta")]/span[contains(@class, "author")]/a/text()',
				root,
				null,
				XPathResult.FIRST_ORDERED_NODE_TYPE,
				null).singleNodeValue.data.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
			if (author == thisAuthor) { author = '<span style="color:rgb(0,191,243);">' + author + '</span>'; }
			
			fpmod = document.evaluate(
				'.//div[contains(@class, "fullpost")]',
				root,
				null,
				XPathResult.FIRST_ORDERED_NODE_TYPE,
				null).singleNodeValue.className;
			//alert(fpmod);
			
			if (fpmod.indexOf('fpmod_nws') != -1) {
				fpmod = '<div class="shackhotkeys_overview_modbullet" style="background-color: rgb(240, 46, 1);"></div>';
			} else if (fpmod.indexOf('fpmod_informative') != -1) {
				fpmod = '<div class="shackhotkeys_overview_modbullet" style="background-color: rgb(0, 191, 243);"></div>';
			} else if (fpmod.indexOf('fpmod_political') != -1) {
				fpmod = '<div class="shackhotkeys_overview_modbullet" style="background-color: rgb(248, 165, 0);"></div>';
			} else if (fpmod.indexOf('fpmod_offtopic') != -1) {
				fpmod = '<div class="shackhotkeys_overview_modbullet" style="background-color: rgb(125, 125, 125);"></div>';
			} else if (fpmod.indexOf('fpmod_stupid') != -1) {
				fpmod = '<div class="shackhotkeys_overview_modbullet" style="background-color: rgb(55, 151, 0);"></div>';
			} else {
				fpmod = '<div class="shackhotkeys_overview_bullet"></div>';
			}

			overviewItem.id = 'shackhotkeys_overview_' + root.id.replace('item_', '');
			overviewItem.setAttribute('class', 'shackhotkeys_overview_item');
			//overviewItem.innerHTML = 'post #' + (i+1) + ': ' + overviewItem.id;

			overviewItem.innerHTML =
					fpmod
					+ '<div style="float: left; margin-left: 2px;">' + postExcerpt + '</div>'
					+ '<div style="color:rgb(255,186,0); float:left; margin-right: 2px;">'
						+ '<span style="color:rgb(152,152,152); float: left;">&nbsp:&nbsp</span>'
						+ author + '</div>'
					//+ '<br />'
					//+ '<div style="clear: right; line-height: 0; height: 0;">&nbsp;</div>'
					+ '<div style="clear: both; line-height: 0; height: 0;">&nbsp;</div>'

			overviewItem.addEventListener('click', function(event) {
					var rootId = this.id.replace('shackhotkeys_overview_', 'root_');
					var root = document.getElementById(rootId);
					window.scrollTo(0, calculateYPos(root) - (window.innerHeight * CONTEXT_RATIO));
					setLastClickedRoot(rootId.replace(/root_/, ''));
					
					// Remove highlighted item
					var highlightedItem = document.getElementById('keysel');
					if (highlightedItem) { highlightedItem.id = '' }

					// Set last-clicked-item on new root.
					var openItem = xGetLast('.//li[contains(@class, "sel")]', root);
					contentWindow.sLastClickedItem = openItem.id.replace(/item_/, '');
					
					// Set highlight on open item
					var openItemHighlight = xGetFirst('.//span[contains(@class, "oneline_body")]', openItem);
					if (openItemHighlight) { openItemHighlight.id = 'keysel'; }
				}, false);

			overviewItem.addEventListener('mouseover', function(event) {
				if (this.id.replace('shackhotkeys_overview_', '') != contentWindow.sLastClickedRoot) {
					this.style.backgroundColor = OVERVIEW_ITEM_COLOR_HOVER;
				}
			}, false);

			overviewItem.addEventListener('mouseout', function(event) {
				if (this.id.replace('shackhotkeys_overview_', '') == contentWindow.sLastClickedRoot) {
					this.style.backgroundColor = OVERVIEW_ITEM_COLOR_SEL;
					//this.style.fontWeight = 'bold';
				} else {
					this.style.backgroundColor = OVERVIEW_ITEM_COLOR;
					//this.style.fontWeight = 'normal';
				}
			}, false);

			overview.appendChild(overviewItem);
		}

		if (TRANSPARENT_OVERVIEW) {
			overviewBkg = overview.cloneNode(true);
			overviewBkg.id = 'shackhotkeys_overview_bkg';
			overviewBkg.style.backgroundColor = OVERVIEW_COLOR;
			document.body.appendChild(overviewBkg);
			overview.style.backgroundColor = 'transparent';
		} else {
			overview.style.backgroundColor = OVERVIEW_COLOR;
		}
		overview.id = 'shackhotkeys_overview';
		document.body.appendChild(overview);

		if (DEBUG) {
			if (GM_log) { GM_log('Overview construction time: ' + (getTime() - scriptStartTime) + 'ms'); }
		}
		if (GM_setValue) { GM_setValue('showOverview', true); }
	}

	function setLastClickedRoot(rootId) {
		var overviewItem;

		overviewItem = document.getElementById('shackhotkeys_overview_' + contentWindow.sLastClickedRoot);
		if (overviewItem) {
			overviewItem.style.backgroundColor = OVERVIEW_ITEM_COLOR;
			//overviewItem.style.fontWeight = 'normal';
		}

		contentWindow.sLastClickedRoot = rootId;
		overviewItem = document.getElementById('shackhotkeys_overview_' + rootId);
		if (overviewItem) {
			overviewItem.style.backgroundColor = OVERVIEW_ITEM_COLOR_SEL;
			//overviewItem.style.fontWeight = 'bold';
		}
	}

	// Override original check_event_target with input -> text fix (textbox event targets in Fx are 'text' not 'input')
	//   might be 'input' for IE, dunno.
	if (!contentWindow.shackhotkeys_check_event_target) {
		
		contentWindow.shackhotkeys_check_event_target = contentWindow.check_event_target;
		contentWindow.check_event_target = my_check_event_target;
	}

	function my_check_event_target( ev )
	{
		if ( ev.target )
		{
			//alert('a');
			var isHandleEvent = ev.target.type != "textarea" && ev.target.type != "text";
			//if (isHandleEvent) {
			//	alert('Must handle event target type: ' + ev.target.type);
			//} else {
			//	alert('SKIP! ' + ev.target.type);
			//}
			return isHandleEvent;
		}
		else if ( ev.srcElement )
		{
			//alert('b');
			return !ev.srcElement.isTextEdit;
		}
		else
		{
			//alert('c');
			return false;
		}
	}

	function shackKeyHandler(event) {

		if (event.altKey || event.ctrlKey || event.metaKey) { return; }
		if (!contentWindow.check_event_target(event)) { return; }

		var key = String.fromCharCode(event.charCode);	// Change this if IE.

		var isDown = false;
		var newRoot = null;

		switch (key) {
			case 'h':
				// Go to first comment
				newRoot = xGetFirst('.//div[contains(@class, "root")]', document.getElementById('recent'));
			case 'l':
				// Go to last comment
				if (!newRoot) {
					newRoot = xGetLast('.//div[contains(@class, "root")]', document.getElementById('recent'));
				}
			case 'm':
				// Go to comment at middle
				if (!newRoot) {
					newRoot = xGetMiddle('.//div[contains(@class, "root")]', document.getElementById('recent'));
				}
			case 'j':
				// Go down one comment thread
				isDown = true;
			case 'k':
				// Go up one comment thread

				if (!newRoot) { newRoot = getNewRoot(isDown); }
				if (!newRoot) { /*alert('tada!');*/ break; }
				window.scrollTo(0, calculateYPos(newRoot) - (window.innerHeight * CONTEXT_RATIO));
				//var targetOffset = calculateYPos(newRoot) - (window.innerHeight * CONTEXT_RATIO);
				//$('html, body').animate({scrollTop: targetOffset}, 'normal', 'swing');
				setLastClickedRoot(newRoot.id.replace(/root_/, ''));

				// Remove highlighted item
				var highlightedItem = document.getElementById('keysel');
				if (highlightedItem) { highlightedItem.id = '' }

				// Set last-clicked-item on new root.
				var openItem = xGetLast('.//li[contains(@class, "sel")]', newRoot);
				if (!openItem) { alert('ouch'); break; }	// should never happen. Root has item_.
				contentWindow.sLastClickedItem = openItem.id.replace(/item_/, '');
				
				// Set highlight on open item
				var openItemHighlight = xGetFirst('.//span[contains(@class, "oneline_body")]', openItem);
				if (openItemHighlight) { openItemHighlight.id = 'keysel'; }

				break;

			case 'J':
				isDown = true;
			case 'K':
				var firstVisible = getFirstVisible('.//div[contains(@class, "root")]', document.getElementById('recent'));
				if (contentWindow.sLastClickedRoot == -1) {
					setLastClickedRoot(firstVisible.id.replace(/root_/, ''));
					contentWindow.sLastClickedItem = contentWindow.sLastClickedRoot;
				}

				var anchorId;
				var highlightedItem = document.getElementById('keysel');

				anchorId = (highlightedItem) ? getItemId(highlightedItem) : contentWindow.sLastClickedItem;

				var nextItemId = null;
				var nextItem = null;

				nextItem = getFirstVisibleHighlight(contentWindow.sLastClickedRoot, anchorId, isDown);

				if (nextItem) {

					var highlightNext = xGetFirst('.//span[contains(@class, "oneline_body")]', nextItem);
					if (highlightNext) {

						// Don't allow user to scroll to root (use 'q' instead)
						var highlightNextItemId = getItemId(highlightNext);
						if (contentWindow.sLastClickedRoot == highlightNextItemId) { break; }

						// Don't allow user to scroll up past cap (WTF5)
						// ... MEH...
						//var threshold = 0;
						//if (contentWindow.sLastClickedRoot.className.indexOf('capped') != -1) {
						//	var capnote = xGetFirst('.//p[@class="capnote"]', root);
						//	threshold = calculateYPosBox(capnote) + 5; // FIVE IS A MAGIC NUMBER, WTF5
						//}

						// Remove previous highlighted item
						if (highlightedItem) { highlightedItem.id = '' }

						highlightNext.id = 'keysel';

						// Check if the highlight line is off the screen
						// Not using isVisible because I'm too lazy to fix it if it breaks something else
						var hlY, hlY2 = 0;
						if (document.getBoxObjectFor) {
							var hlNextBox = document.getBoxObjectFor(highlightNext);
							hlY = hlNextBox.y;
							hlY2 = hlNextBox.y + hlNextBox.height;
						} else {
							hlY = calculateYPos(highlightNext);
							hlY2 = hlY; // find a way to add height of element to hlY2 (for Safari and Opera)
						}
						if (hlY2 > window.pageYOffset + window.innerHeight) {
							highlightNext.scrollIntoView(false);	// align to bottom of screen
						} else if (hlY < window.pageYOffset) {
							highlightNext.scrollIntoView(true);		// align to top
						}

						// User has scrolled up into an open post
						if (contentWindow.sLastClickedItem == highlightNextItemId) {
							var openItem = document.getElementById('item_' + contentWindow.sLastClickedItem);

							if (calculateYPos(openItem) < window.pageYOffset) {	// if root is not all visible
								openItem.scrollIntoView(false);
								window.scrollBy(0, 0 - (window.innerHeight * CONTEXT_RATIO));
							}
						}
					}
				}
				break;

			case 'v':
			//case 'o':
				// Open/close highlighted item, only if in view.
				var highlightedItem = document.getElementById('keysel');

				if (highlightedItem) {
					var highlightedItemId = getItemId(highlightedItem);

					// Don't call close or anything on root.
					if (highlightedItemId == contentWindow.sLastClickedRoot) { break; }

					if (highlightedItemId != contentWindow.sLastClickedItem) {
						if (isVisible(highlightedItem)) {
							//alert('is visible');
							simulateClick(highlightedItem.parentNode, 'click');
						}
					} else {
						// highlight is clicked... have to close this. isVisible(highlightedItem) always false.
						if (isVisible(document.getElementById('item_' + contentWindow.sLastClickedItem))) {
							contentWindow.close_post(contentWindow.sLastClickedItem);
							contentWindow.sLastClickedItem = contentWindow.sLastClickedRoot;
						}
					}
				}
				break;

			case 'X':
				// Collapse... in very strange ways :)

				if (contentWindow.sLastClickedRoot != -1) {

					var root = document.getElementById('root_' + contentWindow.sLastClickedRoot);
					if (root.className.indexOf('collapsed') != -1) {
						contentWindow.show_post(contentWindow.sLastClickedRoot);
						break;
					} else {
						// always collapse root (use v,o to close post)
						contentWindow.close_post(contentWindow.sLastClickedRoot);
						//contentWindow.close_post(contentWindow.sLastClickedItem);
					}
				}
				break;

			case 'q':
				// Focus on root.
				if (contentWindow.sLastClickedRoot == -1 || contentWindow.sLastClickedItem == -1) {
					break;
				}
				contentWindow.sLastClickedItem = contentWindow.sLastClickedRoot;

				// Remove highlighted item
				var highlightedItem = document.getElementById('keysel');
				if (highlightedItem) { highlightedItem.id = '' }

				// Set highlight on root
				var root = document.getElementById('root_' + contentWindow.sLastClickedRoot);
				// Set highlight on open item
				var rootHighlight = xGetFirst('.//span[contains(@class, "oneline_body")]', root);
				if (rootHighlight) { rootHighlight.id = 'keysel'; }

				// Scroll into view
				if (calculateYPos(root) < window.pageYOffset) {	// if root is not all visible
					root.scrollIntoView(false);
					window.scrollBy(0, 0 - (window.innerHeight * CONTEXT_RATIO));
				}
				
				break;

			case 'C':
			//case 'N':
				// New Comment
				window.scrollTo(0, calculateYPos(document.getElementById('newcommentbutton')));
				contentWindow.new_comment();
				break;

			case 'r':
				// Reply
				if (contentWindow.sLastClickedItem != -1) {
					contentWindow.move_postbox_here(contentWindow.sLastClickedItem);
				}
				break;

			case 'f':
				// Refresh
				if (contentWindow.sLastClickedItem != -1) {
					var item = document.getElementById('item_' + contentWindow.sLastClickedItem);
					var link = xGetFirst('.//div[@class="refresh"]/a', item);
					//simulateClick(link, 'click');	// doesn't work, no onclick?
					contentWindow.open(link.href, link.target, '', true);
				}
				break;

			case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
				// Imageloader and PostGet links trigger
				if (contentWindow.sLastClickedItem != -1) {
					//if (!isVisible(document.getElementById('item_' + contentWindow.sLastClickedItem))) { break; }
					var item = document.getElementById('item_' + contentWindow.sLastClickedItem);
					var postbody = xGetFirst('.//div[contains(@class, "postbody")]', item)
					var links = xGetLinksFiltered('.//a', postbody);
					//var links = xGetLinks('.//a', postbody);

					var link = links[key - 1];
					if (link) {
						if (link.className.indexOf('shackimgloader') != -1 || link.className.indexOf('shackpostget') != -1) {
							simulateClick(link, 'click');
						} else {
							GM_openInTab(link.href);
						}
					}
				}
				break;

			case '0':
				// Imageloader load_all, hide_all trigger
				if (contentWindow.sLastClickedItem != -1) {
					if (!isVisible(document.getElementById('item_' + contentWindow.sLastClickedItem))) { break; }
					var link = document.getElementById('shackimgloader_' + contentWindow.sLastClickedItem);
					if (link) { simulateClick(link, 'click'); }
				}
				break;

			//case 'l':
			//	// LOL
			//	if (contentWindow.sLastClickedItem != -1) {
			//		if (!isVisible(document.getElementById('item_' + contentWindow.sLastClickedItem))) { break; }
			//		var link = document.getElementById('lol' + contentWindow.sLastClickedItem);
			//		if (link) { simulateClick(link, 'click'); }
			//	}
			//	break;

			//case 'm':
			//	// Message This User
			//	if (contentWindow.sLastClickedItem != -1) {
			//		if (!isVisible(document.getElementById('item_' + contentWindow.sLastClickedItem))) { break; }
			//		var item = document.getElementById('item_' + contentWindow.sLastClickedItem);
			//		var link = xGetFirst('.//a[starts-with(@title, "Shackmessage")]', item);

			//		if (link) { simulateClick(link, 'click'); }
			//	}
			//	break;

			case 's':
				// Reveal all spoilers
				if (contentWindow.sLastClickedItem != -1) {
					if (!isVisible(document.getElementById('item_' + contentWindow.sLastClickedItem))) { break; }
					var item = document.getElementById('item_' + contentWindow.sLastClickedItem);
					var postbody = xGetFirst('.//div[@class="postbody"]', item)
					var links = xGetLinks('.//span[@class="jt_spoiler"]', postbody);

					for (var i = 0; i < links.length; i++) {
						simulateClick(links[i], 'click');
					}
				}
				break;

			//case 'i':
			//case 'e':
			//	toggleOverview();
			//	break;

			case '?':
				toggleHelp();
				break;

			case '>':
				isDown = true;
			case '<':
				// Next, Previous page.
				var nextprevs = document.evaluate(
						'.//div[@class = "pagenavigation"]/*[@class = "nextprev"]',
						document.getElementById('recent'),
						null,
						XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
						null);

				var nextprev = (isDown) ? nextprevs.snapshotItem(1) : nextprevs.snapshotItem(0);
				if (!nextprev) { break; }
				if (nextprev.href) { contentWindow.open(nextprev.href, '_self', '', true); }
				break;
		}
	}

	// Start here.

	// Override original clickItem with clickItem + set highlight
	if (!contentWindow.shackhotkeys_clickItem) {
		
		contentWindow.shackhotkeys_clickItem = contentWindow.clickItem;
		contentWindow.clickItem = function(root, id) {

			setLastClickedRoot(root);

			// Call original function
			var retval = contentWindow.shackhotkeys_clickItem(root, id);

			// Remove highlighted item
			var highlightedItem = document.getElementById('keysel');
			if (highlightedItem) { highlightedItem.id = '' }

			// Set highlight
			var openItem = document.getElementById('item_' + id);
			var openItemHighlight = xGetFirst('.//span[contains(@class, "oneline_body")]', openItem);
			if (openItemHighlight) { openItemHighlight.id = 'keysel'; }

			return retval;
		};
	}

	if (location.pathname == '/laryn.x') {

		window.addEventListener('keypress', shackKeyHandler, true);

		GM_addStyle("[CDATA[\
			#keysel {\
				background-color: rgb(60, 80, 90);\
			}\
			#shackhotkeys_help_bkg {\
				padding-top: 1%;\
				padding-bottom: 1%;\
				padding-left: 1%;\
				padding-right: 1%;\
				position: fixed;\
				top: 1%;\
				right: 1%;\
				opacity: 0.75;\
				z-index: 2001;\
			}\
			#shackhotkeys_help {\
				padding-top: 1%;\
				padding-bottom: 1%;\
				padding-left: 1%;\
				padding-right: 1%;\
				position: fixed;\
				top: 1%;\
				right: 1%;\
				z-index: 2002;\
			}\
			.shackhotkeys_help_title {\
				font-weight: bold;\
				border-bottom: 1px;\
				border-top: 0px;\
				border-left: 0px;\
				border-right: 0px;\
				border-style: solid;\
				border-color: rgb(128, 128, 128);\
			}\
			.shackhotkeys_help_subtitle {\
				color: rgb(128, 128, 128);\
			}\
			#shackhotkeys_overview_bkg {\
				/*max-width: 25%;*/\
				position: fixed;\
				bottom: 5px;\
				right: 5px;\
				padding-left: 5px;\
				padding-top: 2px;\
				padding-bottom: 2px;\
				padding-right: 5px;\
				opacity: 0.90;\
				z-index: 1001;\
			}\
			#shackhotkeys_overview {\
				/*max-width: 25%;*/\
				position: fixed;\
				bottom: 5px;\
				right: 5px;\
				padding-left: 5px;\
				padding-top: 2px;\
				padding-bottom: 2px;\
				padding-right: 5px;\
				z-index: 1002;\
			}\
			.shackhotkeys_overview_title {\
				/*font-size: 14px;*/\
				padding-left: 5px; /*19px;*/\
				padding-top: 1px;\
				padding-bottom: 1px;\
				font-weight: bold;\
				color: rgb(255, 186, 0);\
				border-bottom: 1px;\
				border-top: 0px;\
				border-left: 0px;\
				border-right: 0px;\
				border-style: solid;\
				border-color: rgb(255, 186, 0);\
			}\
			.shackhotkeys_overview_close {\
				position: absolute;\
				right: 5px;\
				top: 2px;\
				padding-left: 5px;\
				padding-right: 5px;\
				font-size: 11px;\
				border-style: solid;\
				border-width: 1px;\
				border-color: rgb(124, 124, 105);\
				cursor: pointer;\
				text-align: center;\
				background-color: rgb(75, 75, 75);\
				/*text-transform: uppercase;*/\
			}\
			.shackhotkeys_overview_item {\
				/*width: 50%;*/\
				padding-top: 1px;\
				padding-bottom: 1px;\
				cursor: pointer;\
			}\
			.shackhotkeys_overview_bullet {\
				background-image: url(http://cdn.shacknews.com/public/shacknews/sprite-commentslink.gif);\
				background-repeat: none;\
				overflow: hidden;\
				width: 16px;\
				height: 16px;\
				background-position: -50px top;\
				float: left;\
				margin: 0px;\
				padding: 0px;\
			}\
			.shackhotkeys_overview_modbullet {\
				overflow: hidden;\
				width: 16px;\
				height: 12px;\
				margin-top: 3px;\
				margin-bottom: 2px;\
				float: left;\
				opacity: 0.65;\
			}");

		if (GM_getValue) {
			var showOverview = (GM_getValue('showOverview', false) == "true");
			var showHelp = (GM_getValue('showHelp', false) == "true");
			if (showOverview) { toggleOverview(); }
			if (showHelp) { toggleHelp(); }
		}
	}

	if (DEBUG) { if (GM_log) { GM_log((getTime() - scriptStartTime) + 'ms'); }}

})();

}